package com.listeningframework.boot.cors;

import com.listeningframework.boot.cors.CorsConstants.CORSRequestType;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.*;

/**
 * Ajax跨域Filter解决方案
 */
@Component
@Order(Integer.MIN_VALUE)
@EnableConfigurationProperties(CorsProperties.class)
@ConditionalOnProperty(prefix = "listening.cors", name = "enabled", havingValue = "true", matchIfMissing = false)
public final class AppCorsFilter implements Filter {
    /**
     * 日志
     */
    private static final Logger log = LoggerFactory.getLogger(AppCorsFilter.class);
    /**
     * A {@link Collection} of origins consisting of zero or more origins that
     * are allowed access to the resource.
     */
    private final Collection<String> allowedOrigins = new ArrayList<String>();
    /**
     * A {@link Collection} of methods consisting of zero or more methods that
     * are supported by the resource.
     */
    private final Collection<String> allowedHttpMethods = new HashSet<String>();
    /**
     * A {@link Collection} of headers consisting of zero or more header field
     * names that are supported by the resource.
     */
    private final Collection<String> allowedHttpHeaders = new HashSet<String>();
    /**
     * A {@link Collection} of exposed headers consisting of zero or more header
     * field names of headers other than the simple response headers that the
     * resource might use and can be exposed.
     */
    private final Collection<String> exposedHeaders = new HashSet<String>();
    /**
     * Determines if any origin is allowed to make request.
     */
    private boolean anyOriginAllowed;
    /**
     * A supports credentials flag that indicates whether the resource supports
     * user credentials in the request. It is true when the resource does and
     * false otherwise.
     */
    private boolean supportsCredentials;
    /**
     * Indicates (in seconds) how long the results of a pre-flight request can
     * be cached in a pre-flight result cache.
     */
    private long preflightMaxAge = 0L;
    /**
     * Determines if the request should be decorated or not.
     */
    private boolean decorateRequest;
    /**
     * Cros Config Settings
     */
    @Autowired
    private CorsProperties corsProperties;

    @Override
    public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
                         final FilterChain filterChain) throws IOException, ServletException {
        if (!(servletRequest instanceof HttpServletRequest) || !(servletResponse instanceof HttpServletResponse)) {
            throw new ServletException("CORS doesn't support non-HTTP request or response");
        }
        // Safe to downcast at this point.
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String requestURL = request.getRequestURI();
        String compareChar = request.getContextPath() + "/data/ws/**";
        if (!corsProperties.isEnabled() || new AntPathMatcher().match(compareChar, requestURL)) {
            filterChain.doFilter(request, response);
            return;
        }
        // Determines the CORS request type.
        CORSRequestType requestType = checkRequestType(request);

        // Adds CORS specific attributes to request.
        if (decorateRequest) {
            AppCorsFilter.decorateCORSProperties(request, requestType);
        }

        switch (requestType) {
            case SIMPLE:
                // Handles a Simple CORS request.
                this.handleSimpleCORS(request, response, filterChain);
                break;
            case ACTUAL:
                // Handles an Actual CORS request.
                this.handleSimpleCORS(request, response, filterChain);
                break;
            case PRE_FLIGHT:
                // Handles a Pre-flight CORS request.
                this.handlePreflightCORS(request, response, filterChain);
                break;
            case NOT_CORS:
                // Handles a Normal request that is not a cross-origin request.
                this.handleNonCORS(request, response, filterChain);
                break;
            default:
                // Handles a CORS request that violates specification.
                this.handleInvalidCORS(request, response, filterChain);
                break;
        }
    }

    /**
     * // Initialize defaults
     * parseAndStore(CorsConstants.DEFAULT_ALLOWED_ORIGINS,
     * CorsConstants.DEFAULT_ALLOWED_HTTP_METHODS,
     * CorsConstants.DEFAULT_ALLOWED_HTTP_HEADERS,
     * CorsConstants.DEFAULT_EXPOSED_HEADERS,
     * CorsConstants.DEFAULT_SUPPORTS_CREDENTIALS,
     * CorsConstants.DEFAULT_PREFLIGHT_MAXAGE,
     * CorsConstants.DEFAULT_DECORATE_REQUEST);
     * <p>
     * if (filterConfig != null) {
     * String configAllowedOrigins =
     * filterConfig.getInitParameter(CorsConstants.PARAM_CORS_ALLOWED_ORIGINS);
     * String configAllowedHttpMethods =
     * filterConfig.getInitParameter(CorsConstants.PARAM_CORS_ALLOWED_METHODS);
     * String configAllowedHttpHeaders =
     * filterConfig.getInitParameter(CorsConstants.PARAM_CORS_ALLOWED_HEADERS);
     * String configExposedHeaders =
     * filterConfig.getInitParameter(CorsConstants.PARAM_CORS_EXPOSED_HEADERS);
     * String configSupportsCredentials =
     * filterConfig.getInitParameter(CorsConstants.
     * PARAM_CORS_SUPPORT_CREDENTIALS);
     * String configPreflightMaxAge =
     * filterConfig.getInitParameter(CorsConstants.PARAM_CORS_PREFLIGHT_MAXAGE);
     * String configDecorateRequest =
     * filterConfig.getInitParameter(CorsConstants.PARAM_CORS_REQUEST_DECORATE);
     * <p>
     * parseAndStore(configAllowedOrigins, configAllowedHttpMethods,
     * configAllowedHttpHeaders, configExposedHeaders,
     * configSupportsCredentials,
     * configPreflightMaxAge, configDecorateRequest);
     * }
     *
     * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
     */
    @Override
    public void init(final FilterConfig filterConfig) throws ServletException {
        Assert.notNull(corsProperties, "listening properties can not be null");
        parseAndStore(filterConfig, corsProperties);
    }

    /**
     * Handles a CORS request of type {@link CORSRequestType}.SIMPLE.
     *
     * @param request     The {@link HttpServletRequest} object.
     * @param response    The {@link HttpServletResponse} object.
     * @param filterChain The {@link FilterChain} object.
     * @throws IOException 异常
     * @throws ServletException 异常
     * @see <a href="http://www.w3.org/TR/cors/#resource-requests">Simple
     * Cross-Origin Request, Actual Request, and Redirects</a>
     */
    protected void handleSimpleCORS(final HttpServletRequest request, final HttpServletResponse response,
                                    final FilterChain filterChain) throws IOException, ServletException {

        CORSRequestType requestType = checkRequestType(request);
        if (!(requestType == CORSRequestType.SIMPLE || requestType == CORSRequestType.ACTUAL)) {
            String errorMsg = MessageFormat.format("Expects object [{0}] or [{1}]", CORSRequestType.SIMPLE,
                    CORSRequestType.ACTUAL);
            throw new IllegalArgumentException(errorMsg);
        }

        final String origin = request.getHeader(CorsConstants.REQUEST_HEADER_ORIGIN);
        final String method = request.getMethod();

        // Section 6.1.2
        if (!isOriginAllowed(origin) || !allowedHttpMethods.contains(method)) {
            handleInvalidCORS(request, response, filterChain);
            return;
        }

        // Section 6.1.3
        // Add a single Access-Control-Allow-Origin header.
        if (anyOriginAllowed && !supportsCredentials) {
            // If resource doesn't support credentials and if any origin is
            // allowed
            // to make CORS request, return header with '*'.
            response.addHeader(CorsConstants.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*");
        } else {
            // If the resource supports credentials add a single
            // Access-Control-Allow-Origin header, with the value of the Origin
            // header as value.
            response.addHeader(CorsConstants.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, origin);
        }

        // Section 6.1.3
        // If the resource supports credentials, add a single
        // Access-Control-Allow-Credentials header with the case-sensitive
        // string "true" as value.
        if (supportsCredentials) {
            response.addHeader(CorsConstants.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
        }

        // Section 6.1.4
        // If the list of exposed headers is not empty add one or more
        // Access-Control-Expose-Headers headers, with as values the header
        // field names given in the list of exposed headers.
        if ((exposedHeaders != null) && (exposedHeaders.size() > 0)) {
            String exposedHeadersString = join(exposedHeaders, ",");
            response.addHeader(CorsConstants.RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, exposedHeadersString);
        }

        // Forward the request down the filter chain.
        filterChain.doFilter(request, response);
    }

    /**
     * Handles CORS pre-flight request.
     *
     * @param request     The {@link HttpServletRequest} object.
     * @param response    The {@link HttpServletResponse} object.
     * @param filterChain The {@link FilterChain} object.
     * @throws IOException 异常
     * @throws ServletException 异常
     */
    protected void handlePreflightCORS(final HttpServletRequest request, final HttpServletResponse response,
                                       final FilterChain filterChain) throws IOException, ServletException {

        CORSRequestType requestType = checkRequestType(request);
        if (requestType != CORSRequestType.PRE_FLIGHT) {
            throw new IllegalArgumentException(
                    MessageFormat.format("Expects HttpServletRequest type [{0}]", CORSRequestType.PRE_FLIGHT.name()));
        }

        final String origin = request.getHeader(CorsConstants.REQUEST_HEADER_ORIGIN);

        // Section 6.2.2
        if (!isOriginAllowed(origin)) {
            handleInvalidCORS(request, response, filterChain);
            return;
        }

        // Section 6.2.3
        String accessControlRequestMethod = request
                .getHeader(CorsConstants.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD);
        if (accessControlRequestMethod == null
                || !CorsConstants.HTTP_METHODS.contains(accessControlRequestMethod.trim())) {
            handleInvalidCORS(request, response, filterChain);
            return;
        } else {
            accessControlRequestMethod = accessControlRequestMethod.trim();
        }

        // Section 6.2.4
        String accessControlRequestHeadersHeader = request
                .getHeader(CorsConstants.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS);
        List<String> accessControlRequestHeaders = new LinkedList<String>();
        if (accessControlRequestHeadersHeader != null && !accessControlRequestHeadersHeader.trim().isEmpty()) {
            String[] headers = accessControlRequestHeadersHeader.trim().split(",");
            for (String header : headers) {
                accessControlRequestHeaders.add(header.trim().toLowerCase());
            }
        }

        // Section 6.2.5
        if (!allowedHttpMethods.contains(accessControlRequestMethod)) {
            handleInvalidCORS(request, response, filterChain);
            return;
        }

        // Section 6.2.6
        if (!accessControlRequestHeaders.isEmpty()) {
            for (String header : accessControlRequestHeaders) {
                if (!allowedHttpHeaders.contains(header)) {
                    handleInvalidCORS(request, response, filterChain);
                    return;
                }
            }
        }

        // Section 6.2.7
        if (supportsCredentials) {
            response.addHeader(CorsConstants.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, origin);
            response.addHeader(CorsConstants.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
        } else {
            if (anyOriginAllowed) {
                response.addHeader(CorsConstants.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*");
            } else {
                response.addHeader(CorsConstants.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, origin);
            }
        }

        // Section 6.2.8
        if (preflightMaxAge > 0) {
            response.addHeader(CorsConstants.RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE, String.valueOf(preflightMaxAge));
        }

        // Section 6.2.9
        response.addHeader(CorsConstants.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS, accessControlRequestMethod);

        // Section 6.2.10
        if ((allowedHttpHeaders != null) && (!allowedHttpHeaders.isEmpty())) {
            response.addHeader(CorsConstants.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
                    join(allowedHttpHeaders, ","));
        }
    }

    /**
     * Handles a request, that's not a CORS request, but is a valid request i.e.
     * it is not a cross-origin request. This implementation, just forwards the
     * request down the filter chain.
     *
     * @param request     The {@link HttpServletRequest} object.
     * @param response    The {@link HttpServletResponse} object.
     * @param filterChain The {@link FilterChain} object.
     * @throws IOException
     * @throws ServletException
     */
    private void handleNonCORS(final HttpServletRequest request, final HttpServletResponse response,
                               final FilterChain filterChain) throws IOException, ServletException {
        filterChain.doFilter(request, response);
    }

    /**
     * Handles a CORS request that violates specification.
     *
     * @param request     The {@link HttpServletRequest} object.
     * @param response    The {@link HttpServletResponse} object.
     * @param filterChain The {@link FilterChain} object.
     */
    private void handleInvalidCORS(final HttpServletRequest request, final HttpServletResponse response,
                                   final FilterChain filterChain) {
        String origin = request.getHeader(CorsConstants.REQUEST_HEADER_ORIGIN);
        String method = request.getMethod();
        String accessControlRequestHeaders = request
                .getHeader(CorsConstants.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS);

        response.setContentType("text/plain");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.resetBuffer();

        // Debug so no need for i18n
        StringBuilder message = new StringBuilder("Invalid CORS request; Origin=");
        message.append(origin);
        message.append(";Method=");
        message.append(method);
        if (accessControlRequestHeaders != null) {
            message.append(";Access-Control-Request-Headers=");
            message.append(accessControlRequestHeaders);
        }
        log.info(message.toString());
    }

    @Override
    public void destroy() {
        // NOOP
    }

    /**
     * Decorates the {@link HttpServletRequest}, with CORS attributes.
     * <ul>
     * <li><b>cors.isCorsRequest:</b> Flag to determine if request is a CORS
     * request. Set to <code>true</code> if CORS request; <code>false</code>
     * otherwise.</li>
     * <li><b>cors.request.origin:</b> The Origin URL.</li>
     * <li><b>cors.request.type:</b> Type of request. Values:
     * <code>simple</code> or <code>preflight</code> or <code>not_cors</code> or
     * <code>invalid_cors</code></li>
     * <li><b>cors.request.headers:</b> Request headers sent as
     * 'Access-Control-Request-Headers' header, for pre-flight request.</li>
     * </ul>
     *
     * @param request         The {@link HttpServletRequest} object.
     * @param corsRequestType The {@link CORSRequestType} object.
     */
    protected static void decorateCORSProperties(final HttpServletRequest request,
                                                 final CORSRequestType corsRequestType) {
        if (request == null) {
            throw new IllegalArgumentException("HttpServletRequest object is null");
        }

        if (corsRequestType == null) {
            throw new IllegalArgumentException("CORSRequestType object is null");
        }

        switch (corsRequestType) {
            case SIMPLE:
                request.setAttribute(CorsConstants.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, Boolean.TRUE);
                request.setAttribute(CorsConstants.HTTP_REQUEST_ATTRIBUTE_ORIGIN,
                        request.getHeader(CorsConstants.REQUEST_HEADER_ORIGIN));
                request.setAttribute(CorsConstants.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE,
                        corsRequestType.name().toLowerCase());
                break;
            case ACTUAL:
                request.setAttribute(CorsConstants.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, Boolean.TRUE);
                request.setAttribute(CorsConstants.HTTP_REQUEST_ATTRIBUTE_ORIGIN,
                        request.getHeader(CorsConstants.REQUEST_HEADER_ORIGIN));
                request.setAttribute(CorsConstants.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE,
                        corsRequestType.name().toLowerCase());
                break;
            case PRE_FLIGHT:
                request.setAttribute(CorsConstants.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, Boolean.TRUE);
                request.setAttribute(CorsConstants.HTTP_REQUEST_ATTRIBUTE_ORIGIN,
                        request.getHeader(CorsConstants.REQUEST_HEADER_ORIGIN));
                request.setAttribute(CorsConstants.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE,
                        corsRequestType.name().toLowerCase());
                String headers = request.getHeader(CorsConstants.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS);
                if (headers == null) {
                    headers = "";
                }
                request.setAttribute(CorsConstants.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS, headers);
                break;
            case NOT_CORS:
                request.setAttribute(CorsConstants.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, Boolean.FALSE);
                break;
            default:
                // Don't set any attributes
                break;
        }
    }

    /**
     * Joins elements of {@link Set} into a string, where each element is
     * separated by the provided separator.
     *
     * @param elements      The {@link Set} containing elements to join together.
     * @param joinSeparator The character to be used for separating elements.
     * @return The joined {@link String}; <code>null</code> if elements
     * {@link Set} is null.
     */
    protected static String join(final Collection<String> elements, final String joinSeparator) {
        String separator = ",";
        if (elements == null) {
            return null;
        }
        if (joinSeparator != null) {
            separator = joinSeparator;
        }
        StringBuilder buffer = new StringBuilder();
        boolean isFirst = true;
        for (String element : elements) {
            if (!isFirst) {
                buffer.append(separator);
            } else {
                isFirst = false;
            }

            if (element != null) {
                buffer.append(element);
            }
        }
        return buffer.toString();
    }

    protected CORSRequestType checkRequestType(final HttpServletRequest request) {
        // CORSRequestType requestType = CORSRequestType.INVALID_CORS;
        if (request == null) {
            throw new IllegalArgumentException("HttpServletRequest object is null");
        }
        String originHeader = request.getHeader(CorsConstants.REQUEST_HEADER_ORIGIN);
        if (originHeader == null) {
            return CORSRequestType.NOT_CORS;
        }
        String method = request.getMethod();
        if (originHeader.isEmpty() || !isValidOrigin(originHeader) || method == null
                || !CorsConstants.HTTP_METHODS.contains(method)) {
            return CORSRequestType.INVALID_CORS;
        }

        if ("OPTIONS".equals(method)) {
            return checkOptionsRequestType(request);
        }

        if ("GET".equals(method) || "HEAD".equals(method)) {
            return CORSRequestType.SIMPLE;
        }

        if ("POST".equals(method)) {
            return checkPostRequestType(request);
        }
        if (CorsConstants.COMPLEX_HTTP_METHODS.contains(method)) {
            return CORSRequestType.ACTUAL;
        }
        return CORSRequestType.INVALID_CORS;
    }

    private CORSRequestType checkPostRequestType(final HttpServletRequest request) {
        String contentType = request.getContentType();
        if (contentType == null) {
            return CORSRequestType.INVALID_CORS;
        }
        contentType = contentType.toLowerCase().trim();
        if (CorsConstants.SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES.contains(contentType)) {
            return CORSRequestType.SIMPLE;
        }
        return CORSRequestType.ACTUAL;
    }

    private CORSRequestType checkOptionsRequestType(final HttpServletRequest request) {
        String accessControlRequestMethodHeader = request
                .getHeader(CorsConstants.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD);
        if (accessControlRequestMethodHeader != null && !accessControlRequestMethodHeader.isEmpty()) {
            return CORSRequestType.PRE_FLIGHT;
        }
        if (accessControlRequestMethodHeader != null && accessControlRequestMethodHeader.isEmpty()) {
            return CORSRequestType.INVALID_CORS;
        }
        return CORSRequestType.ACTUAL;
    }

    /**
     * Checks if the Origin is allowed to make a CORS request.
     *
     * @param origin The Origin.
     * @return <code>true</code> if origin is allowed; <code>false</code>
     * otherwise.
     */
    private boolean isOriginAllowed(final String origin) {
        return anyOriginAllowed ? true : allowedOrigins.contains(origin);
    }

    /**
     * Init Cors Setting Config
     *
     * @param filterConfig
     * @param corsProperties
     * @throws ServletException
     */
    private void parseAndStore(final FilterConfig filterConfig,
                               final CorsProperties corsProperties) throws ServletException {
        String allowedOrigins = corsProperties.getAllowedOrigins();
        String allowedHttpMethods = corsProperties.getAllowedHttpMethods();
        String allowedHttpHeaders = corsProperties.getAllowedHttpHeaders();
        String exposedHeaders = corsProperties.getExposedHeaders();
        String supportsCredentials = corsProperties.getSupportsCredentials();
        String preflightMaxAge = corsProperties.getPreflightMaxAge();
        String decorateRequest = corsProperties.getDecorateRequest();

        if (allowedOrigins != null && StringUtils.equals(allowedOrigins.trim(), "*")) {
            this.anyOriginAllowed = true;
        }
        if (allowedOrigins != null && !StringUtils.equals(allowedOrigins.trim(), "*")) {
            this.anyOriginAllowed = false;
            Set<String> setAllowedOrigins = parseStringToSet(allowedOrigins);
            this.allowedOrigins.clear();
            this.allowedOrigins.addAll(setAllowedOrigins);
        }

        if (allowedHttpMethods != null) {
            Set<String> setAllowedHttpMethods = parseStringToSet(allowedHttpMethods);
            this.allowedHttpMethods.clear();
            this.allowedHttpMethods.addAll(setAllowedHttpMethods);
        }

        if (allowedHttpHeaders != null) {
            Set<String> setAllowedHttpHeaders = parseStringToSet(allowedHttpHeaders);
            Set<String> lowerCaseHeaders = new HashSet<String>();
            for (String header : setAllowedHttpHeaders) {
                String lowerCase = header.toLowerCase();
                lowerCaseHeaders.add(lowerCase);
            }
            this.allowedHttpHeaders.clear();
            this.allowedHttpHeaders.addAll(lowerCaseHeaders);
        }

        if (exposedHeaders != null) {
            Set<String> setExposedHeaders = parseStringToSet(exposedHeaders);
            this.exposedHeaders.clear();
            this.exposedHeaders.addAll(setExposedHeaders);
        }

        // For any value other then 'true' this will be false.
        if (supportsCredentials != null) {
            this.supportsCredentials = Boolean.parseBoolean(supportsCredentials);
        }

        if (preflightMaxAge != null && StringUtils.isNumeric(preflightMaxAge)) {
            this.preflightMaxAge = Long.parseLong(preflightMaxAge);
        }

        // For any value other then 'true' this will be false.
        if (decorateRequest != null) {
            this.decorateRequest = Boolean.parseBoolean(decorateRequest);
        }
    }

    /**
     * Takes a comma separated list and returns a Set<String>.
     *
     * @param data A comma separated list of strings.
     * @return Set<String>
     */
    private Set<String> parseStringToSet(final String data) {
        String[] splits = new String[]{};
        if (StringUtils.isNotBlank(data)) {
            splits = data.split(",");
        }
        Set<String> set = new HashSet<String>();
        if (splits.length <= 0) {
            return set;
        }
        for (String split : splits) {
            set.add(split.trim());
        }
        return set;
    }

    /**
     * Checks if a given origin is valid or not. Criteria:
     * <ul>
     * <li>If an encoded character is present in origin, it's not valid.</li>
     * <li>Origin should be a valid {@link URI}</li>
     * </ul>
     *
     * @param origin 来源
     * @see <a href="http://tools.ietf.org/html/rfc952">RFC952</a>
     * @return boolean
     */
    protected static boolean isValidOrigin(String origin) {
        // Checks for encoded characters. Helps prevent CRLF injection.
        if (origin.contains("%")) {
            return false;
        }
        URI originURI;
        try {
            originURI = new URI(origin);
        } catch (URISyntaxException e) {
            return false;
        }
        // If scheme for URI is null, return false. Return true otherwise.
        return originURI.getScheme() != null;
    }

    /**
     * Determines if any origin is allowed to make CORS request.
     *
     * @return boolean
     */
    public boolean isAnyOriginAllowed() {
        return anyOriginAllowed;
    }

    /**
     * Returns a {@link Set} of headers that should be exposed by browser.
     *
     * @return Collection
     */
    public Collection<String> getExposedHeaders() {
        return exposedHeaders;
    }

    /**
     * Determines is supports credentials is enabled.
     *
     * @return boolean
     */
    public boolean isSupportsCredentials() {
        return supportsCredentials;
    }

    /**
     * Returns the preflight response cache time in seconds.
     *
     * @return Time to cache in seconds.
     */
    public long getPreflightMaxAge() {
        return preflightMaxAge;
    }

    /**
     * Returns the {@link Set} of allowed origins that are allowed to make
     * requests.
     *
     * @return {@link Set}
     */
    public Collection<String> getAllowedOrigins() {
        return allowedOrigins;
    }

    /**
     * Returns a {@link Set} of HTTP methods that are allowed to make requests.
     *
     * @return {@link Set}
     */
    public Collection<String> getAllowedHttpMethods() {
        return allowedHttpMethods;
    }

    /**
     * Returns a {@link Set} of headers support by resource.
     *
     * @return {@link Set}
     */
    public Collection<String> getAllowedHttpHeaders() {
        return allowedHttpHeaders;
    }

}
